#!/usr/bin/python
#
# File to manage Windows session information.
#
#
import datetime
import os.path
import pywintypes
import subprocess
import sys
import time
import win32api
import win32com.client
import win32con
import win32gui
import win32process
import win32security

from comtypes.client import CreateObject
import urlhistLib

# List of executables to ignore.
paths_to_ignore = [os.path.normcase(path)
                   for path in ['C:\\WINDOWS\\system32\\logon.scr',
                                'C:\\WINDOWS\\system32\\rundll32.exe',
                                'C:\\WINDOWS\\system32\\userinit.exe',
                                'C:\\Program Files\\Adobe\\Reader 9.0\\Reader\\AcroRd32Info.exe',
                                'C:\\PROGRA~1\\FileNET\\IDM\\fnlocdb.exe']]

# List of urls to ignore
ignored_urls = ['http://news.bbc.co.uk/',
                'http://newsrss.bbc.co.uk/',
                'http://www.betanews.com/',
                'http://www.bom.gov.au/',
                'http://mirror.bom.gov.au/'
                'http://150.207.1.67/',
                'res://ieframe.dll/']

# Dictionary of process handles and path to executable indexed by process ID.
processes = {}

# Set of know applications used to monitor application state changes.
known_applications = set()

# Constant returned by GetExitCodeProcess tp indicate the process is stilll running
STILL_ACTIVE = 259

# Set of visited urls indexed by url
known_urls = set()

def find_process(path):
        """Find the process handle for this application return None if process not running."""
        processes = update_processes().values()
        proc_map = dict([(path, proc) for proc, path in processes])
        if path in proc_map:
            process = proc_map[path]
        else:
            process = None
        return process

class NT_Application(object):
    """Generic interface to handle application states."""

    def __init__(self, path):
        """Store the application path."""
        self._has_changed = True
        self.__is_running = False
        self.path = path
        self.name = os.path.splitext(os.path.basename(self.path))[0].title()
        self.process = None
        known_applications.add(self)

    def __eq__(self, other):
        """Compare applications based on path."""
        return type(self) == type(other) and self.path.lower() == other.path.lower()

    def __ne__(self, other):
        """Compare applications based on path."""
        return type(self) != type(other) or self.path.lower() != other.path.lower()

    def __hash__(self):
        """Generate a hash from the application path."""
        return hash(self.path)

    def __str__(self):
        """Return the string representation."""
        return os.path.splitext(os.path.basename(self.path))[0]

    def to_string(self, indent=0):
        """Create an indented string of this application's state."""
        name = os.path.splitext(os.path.basename(self.path))[0]
        name = name.lower()
        string = '%s\n%sPath: %s\n' % (name, ' ' * indent, self.path)
        return string

    def get_children(self):
        """Return list of child items."""
        # Required for interface to list view
        return []

    children = property(get_children)

    def is_running(self):
        """Return a boolean indicating whether the application is running."""
        result = False
        if self.find_process():
            result = True
        return result

    def get_state(self):
        """Return the run state of the application."""
        if self.is_running():
            return 'Running'
        else:
            return 'Stopped'

    state = property(get_state)

    def update(self):
        """Update the state of this object from the target application state."""
        if self.__is_running != self.is_running():
            self._has_changed = True
        self.__is_running = self.is_running()

    def find_process(self):
        """Find the process handle for this application return None if process not running."""
        # If a process is identified, check that it is still valid
        if self.process:
            if win32process.GetExitCodeProcess(self.process) != STILL_ACTIVE:
                self.process = None
        
        # If no process identified check all running processes to see if they match
        if not self.process:
            for pid in win32process.EnumProcesses():
                try:
                    process = win32api.OpenProcess(win32security.POLICY_ALL_ACCESS, False, pid)
                    modules = win32process.EnumProcessModules(process)
                    path = win32process.GetModuleFileNameEx(process, modules[0])
                    path = os.path.normcase(path)
                    if path == self.path:
                        self.process = process
                        self._has_changed = True
                        break
                except pywintypes.error:
                   # Ignore access denied errors while attempting to connect to processes we may not own
                   pass

        return self.process

    def state_changed(self):
        """Return an indication of a change of state of the application."""
        result = self._has_changed
        self._has_changed = False
        return result

    has_changed = property(state_changed)

    def open(self):
        """Display the application."""
        if self.is_running():
            # TODO: Switch to application's window
            pass
        else:
            startup_info = win32process.STARTUPINFO()
            result = win32process.CreateProcess(None,
                                                self.path,
                                                None,
                                                None,
                                                False,
                                                win32process.DETACHED_PROCESS,
                                                None,
                                                None,
                                                startup_info)
            self.process = result[0]
            translations = win32api.GetFileVersionInfo(path, u'\\VarFileInfo\\Translation')
            version_info = {}
            lang, codepage = translations[0]
            for string in version_info_strings:
                version_info[string] = win32api.GetFileVersionInfo(self.path,
                         u'\\StringFileInfo\\%04X%04X\\%s' % (lang, codepage, string))

            processes[result[2]] = (result[0], self.path, version_info)

    def stop_monitoring(self):
        """Stop monitoring the application state."""
        # For basic apps monitoring is via snopshot so no action required
        pass

    def start_monitoring(self):
        """Start monitoring the application state."""
        self.__is_running = self.is_running()
        #self._has_changed = False

    def restore(self):
        """Restore the state of this application."""
        if self.previous_state == 'Running':
            self.open()

class File_Item(object):
    """Simple wrapper around a filename to provide session management functionality."""

    def __init__(self, path, application, title=None):
        """Initialise the object."""
        self.path = os.path.normpath(os.path.normcase(path))
        self.application = application
        self.title = title

    def __str__(self):
        return self.path
        
    def __hash__(self):
        return hash(self.path)

    def __eq__(self, other):
        return type(self) == type(other) and self.path == other.path

    def __ne__(self, other):
        return not (self == other)

    def get_state(self):
        """Return the open state of the file."""
        if self.application.is_running():
            if self.path in self.application.get_open_files():
                return 'Open'
        return 'Closed'
            
    state = property(get_state)

    def get_name(self):
        """Return the name for this file."""
        if self.title:
            name = self.title
        else:
            name = os.path.splitext(os.path.basename(self.path))[0].title()
        return name

    name = property(get_name)

    def update(self):
        """Update the state of the target object from this object."""
        pass

    def get_children(self):
        """Return the children as used by the list view."""
        return []

    children = property(get_children)

    def open(self):
        """View this file."""
        self.application.open(self.path)

    def restore(self):
        """Resotre the state of this file."""
        if self.previous_state == 'Open':
            self.open()

class COM_Application(NT_Application):
    """Handle management of com applications."""
    __monitoring = False
    __path = None

    def __init__(self, path, com_object=None, item_title=None):
        """Initialise the interface required to access the application."""
        NT_Application.__init__(self, path)
        self.item_title = item_title
        self.interface_name = com_object
        self.visited_files = set()            
        self.__open_files = set()
        self.__com_interface = None

        self.name = custom_apps[os.path.basename(self.path).lower()][0]

    def __str__(self):
        """Return the string representation."""
        return os.path.splitext(os.path.basename(self.path))[0]

    def get_com_interface(self):
        """Returns a com interface to the application."""
        # If interface has been established check that it is still valid
        if self.__com_interface:
            try:
                name = self.__com_interface.name
            except:
                self.__com_interface = None
            
        if not self.__com_interface:
            try:
                interface = win32com.client.GetObject(None, self.interface_name)
            except pywintypes.com_error:
                # If the application is running pause and try again to allow time
                # for interface to be created.
                if self.is_running():
                    time.sleep(10)
                    interface = win32com.client.GetObject(None, self.interface_name)                    
                interface = win32com.client.Dispatch(self.interface_name)
                interface.visible = True
            self.__com_interface = interface

        return self.__com_interface

    def find_process(self):
        """Find the process handle for this application return None if process not running."""
        # If a process is identified, check that it is still valid
        if self.process:
            if win32process.GetExitCodeProcess(self.process) != STILL_ACTIVE:
                self.process = None
                self.__com_interface = None
        
        # If no process identified check all running processes to see if they match
        if not self.process:
            for pid in win32process.EnumProcesses():
                try:
                    process = win32api.OpenProcess(win32security.POLICY_ALL_ACCESS, False, pid)
                    modules = win32process.EnumProcessModules(process)
                    path = win32process.GetModuleFileNameEx(process, modules[0])
                    path = os.path.normcase(path)
                    if path == self.path:
                        self.process = process
                        break
                except pywintypes.error:
                   # Ignore access denied errors while attempting to connect to processes we may not own
                   pass

        return self.process

    def get_children(self):
        """Return list of child items."""
        return self.visited_files

    children = property(get_children)

    def update(self):
        """Update the state of this application."""
        if self.__monitoring:
            NT_Application.update(self)
            files = set([File_Item(path, self, title)
                         for path, title
                         in self.get_open_files()
                         if path])
            new_files = files - self.__open_files

            if len(new_files) > 0:
                self.visited_files |= new_files
                self.__open_files = files
                self._has_changed = True

    def get_open_documents(self):
        """Return the list of open items in this application."""
        if not self.__monitoring:
           # Ignore items that do not have a full path
           self.__open_files = set([File_Item(path, self, title)
                                    for path, title
                                    in self.get_open_files()
                                    if path])
        return self.__open_files

    def get_open_files(self):
        """Return the list of files open in this application."""
        open_files = set()
        if self.is_running():
            count = 0
            if self.item_title:
                interface = self.get_com_interface()
                elements = getattr(interface, self.item_title)
                open_files = set([(doc.FullName, doc.Name) for doc in elements if doc.path])
        return open_files

    def start_monitoring(self):
        """Start monitoring the application state."""
        # Initialise the start state.
        if not self.__monitoring:
            NT_Application.start_monitoring(self)
            self.visited_files = set()
            self.get_open_documents()
            self.__monitoring = True

    def stop_monitoring(self):
        """Stop monitoring the application state."""
        if self.__monitoring:
            NT_Application.stop_monitoring(self)
            self.update()
            self.__monitoring = False

    def open(self, path=None):
        """Open/activate the selected item."""
        interface = self.get_com_interface()
        if path:
            elements = getattr(interface, self.item_title)
            count = elements.Count
            for index in range(0, count):
                if elements[index].FullName == path:
                    elements[index].Activate()
            else:
                document = elements.Open(path)
                document.Activate()

        interface.visible = True
        self.start_monitoring()

    def to_string(self, indent=0):
        """Create an indented string of this application's state."""
        string = NT_Application.to_string(self, indent)
        if self.files:
            string += '%sFiles: ' % (' ' * indent)
            indent2 = indent + 7
            sep = '\n%s' % (' ' * indent2)
            string += sep.join([str(file) for file in self.files])
            string += '\n'
        string += '%sCOM Interface: %s\n' % (' ' * indent, self.interface_name)
        return string

    def visit(self, path):
        """Display the application."""
        self.get_com_interface().visible = True

    def restore(self):
        """Restore the state of this application."""
        [item.restore() for item in self.files]
        if self.previous_state == 'Running':
            self.open()

class Win_Shell_Application(NT_Application):
    """Management of Windows Shell applications (Explorer and IE)."""
    __open_urls = set()
    __monitoring = False
    __path = None

    def __init__(self, path, app_name):
        """Initialise the interface required to access the application."""
        NT_Application.__init__(self, path)
        self.visited_urls = set()
        shell_windows_clsid = '{9BA05972-F6A8-11CF-A442-00A0C90A8F39}'
        self.shell_windows = win32com.client.Dispatch(shell_windows_clsid)
        self.name = custom_apps[os.path.basename(self.path).lower()][0]
        self.app_name = app_name

    def __str__(self):
        """Return a string representation."""
        return os.path.splitext(os.path.basename(self.path))[0]

    def get_children(self):
        """Return list of child items."""
        return self.visited_urls

    children = property(get_children)

    def update(self):
        """Update the state of this application."""
        if self.__monitoring:
            NT_Application.update(self)
            urls = set([Visited_Url(window.LocationName,
                                    window.LocationURL)
                                    for window in self.shell_windows
                                    if window.LocationURL and
                                       str(window.Application) == self.app_name])

            new_urls = urls - self.__open_urls
            if len(new_urls) > 0:
                self.visited_urls |= new_urls
                self.__open_urls = urls
                self._has_changed = True

    def get_open_documents(self):
        """Return a list of pages currently open in explorer."""
        if not self.__monitoring:
           self.__open_urls = set([Visited_Url(window.LocationName,
                                   window.LocationURL)
                                   for window in self.shell_windows
                                   if window.LocationURL and
                                       str(window.Application) == self.app_name])
        return self.__open_urls

    def start_monitoring(self):
        """Start monitoring the application state."""
        # Initialise the start state.
        if not self.__monitoring:
            NT_Application.start_monitoring(self)
            self.visited_urls = set()
            self.get_open_documents()
            self.__monitoring = True

    def stop_monitoring(self):
        """Stop monitoring the application state."""
        if self.__monitoring:
            NT_Application.stop_monitoring(self)
            self.update()
            self.__monitoring = False

    def open(self, path=None):
        """Open/activate the selected item."""
        explorer_windows = [browser for browser in self.shell_windows
                            if str(browser.Application) == self.app_name]
                    
        if path and self.is_running():
            for browser in explorer_windows:
                if browser.LocationURL == path:
                    browser.visible = True
                    win32gui.ShowWindow(browser.Application.HWND, win32con.SW_SHOW)
                    win32gui.BringWindowToTop(browser.Application.HWND)
                    break
            else:
                # 2048 = navOpenInNewTab
                # Windows Internet Explorer 7. Open the resource or file in a new tab.
                # Allow the destination window to come to the foreground, if necessary.
                application = explorer_windows[0].Application
                application.Navigate(path, 2048)
                win32gui.ShowWindow(application.HWND, win32con.SW_SHOW)
                win32gui.BringWindowToTop(application.HWND)
        elif self.is_running():
            browser = explorer_windows[0]
            win32gui.ShowWindow(browser.Application.HWND, win32con.SW_SHOW)
            win32gui.BringWindowToTop(browser.Application.HWND)
        else:
            startup_info = win32process.STARTUPINFO()
            command = '%s' % (self.path)
            if path:
                command += ' %s' % path
            result = win32process.CreateProcess(None,
                                                command,
                                                None,
                                                None,
                                                False,
                                                win32process.DETACHED_PROCESS,
                                                None,
                                                None,
                                                startup_info)
            processes[result[2]] = (result[0], self.path)

    def to_string(self, indent=0):
        """Create an indented string of this application's state."""
        string = NT_Application.to_string(self, indent)
        if self.visited_urls:
            string += '%sURLs: ' % (' ' * indent)
            indent2 = indent + 7
            sep = '\n%s' % (' ' * indent2)
            string += sep.join([str(urls) for urls in self.visited_urls])
            string += '\n'
        string += '%sCOM Interface: %s\n' % (' ' * indent, self.interface_name)
        return string

    def restore(self):
        """Restore the state of this application."""
        [item.restore() for item in self.visited_urls]
        if self.previous_state == 'Running':
            self.open()

class Visited_Url(object):
    """Manage URLs."""
    state = ''
    children = []
    _has_changed = False

    def __init__(self, title, url, time=None):
        """Store the data for the URL."""
        self.__title = title
        self.__url = url
        if time:
            self.__time = time
        else:
            self.__time = datetime.datetime.now()
        self._has_changed = True

    def __str__(self):
        """Returns a string representation of the item."""
        url = self.__url.encode('utf-8') if self.__url else None
        if self.__title:
            title = self.__title.encode('utf-8')
        else:
            title = os.path.basename(url)
        return '%s' % (title)

    def __eq__(self, other):
        """Compare based on url only."""
        return type(self) == type(other) and self.__url.lower() == other.url.lower()

    def __ne__(self, other):
        """Compare based on url only."""
        # Ignore caase when comparing urls
        return self.__url.lower() != other.url.lower()

    def __hash__(self):
        """Compare based on url only."""
        return hash(self.__url.lower())

    def state_changed(self):
        """Return an indication of a change of state of the link."""
        # State change only occurs on initial visit
        result = self._has_changed
        self._has_changed = False
        return result

    has_changed = property(state_changed)

    def update(self):
        """Does nothing, dummy function for session management interface"""
        pass

    def get_children(self):
        """Return list of child items."""
        # Required for interface to list view
        return []

    def get_url(self):
        """Return the URL."""
        return self.__url
    
    url = property(get_url)
    path = property(get_url)
    
    def get_title(self):
        """Return the title for the page."""
        if self.__title:
            title = self.__title.encode('utf-8')
        else:
            title = os.path.basename(self.__url)
        return '%s' % (title)
    
    title = property(get_title)
    name = property(get_title)

    def get_time(self):
        """Return the time this URL was visited."""
        return self.__time
    
    time = property(get_time)

    def open(self):
        """Revisit the URL using default operation."""
        win32api.ShellExecute(0, None, self.__url, None, os.getcwd(), 0)

    def restore(self):
        """Restore the state of this item."""
        pass

# Dictionary of application types used to map strings to classes
app_types = {'COM_Application' : COM_Application,
             'Shell_Window_App' : Win_Shell_Application}

# Dictionary of applications which use custom class to determine their state, based on executable name.
custom_apps = {'outlook.exe' : ['Outlook', 'COM_Application', 'Outlook.Application', None],
               'winword.exe' : ['Word', 'COM_Application', 'Word.Application', 'Documents'],
               'excel.exe' : ['Excel', 'COM_Application', 'Excel.Application', 'Workbooks'],
               'iexplore.exe' : ['Internet Explorer', 'Shell_Window_App', 'Windows Internet Explorer'],
               'explorer.exe' : ['Windows Explorer', 'Shell_Window_App', 'Microsoft Internet Explorer'],
               'schdpl32.exe' : ['Schedule Plus', 'COM_Application', 'SchedulePlus.Application', None],
               'powerpnt.exe' : ['PowerPoint', 'COM_Application', 'PowerPoint.Application', 'Presentations'],
               'mmc.exe' : ['Microsoft Management Console', 'COM_Application', 'MMC20.Application', None],
               'graph.exe' : ['MS Graph', 'COM_Application', 'MSGraph.Application', None],
               'voloview.exe' : ['VoloView', 'COM_Application', 'VoloView.Application', None],
               'visio.exe' : ['Visio', 'COM_Application', 'Visio.Application', 'Documents'],
               'doors.exe' : ['DOORS', 'COM_Application', 'DOORS.Application', None],
               #'winfxdocobj.exe' : ['Bootstrap', 'bootstrap.Application', None],
               'msaccess.exe' : ['MS Access', 'COM_Application', 'Access.Application', None],
               'idmview.exe' : ['FileNet', 'COM_Application', 'IDMViewerApp.APPLICATION', None]}

def get_application(path):
    """Return an app or custom app depending on the executable name."""
    app_from_path = dict([(app.path.lower(), app) for app in known_applications])
    if path.lower() in app_from_path: 
        application = app_from_path[path.lower()]
    else:
        exe = os.path.basename(path).lower()
        if exe in custom_apps:
            detail = custom_apps[exe]
            application = app_types[detail[1]](path, *detail[2:])
            application.name = detail[0]
        else:
            application = NT_Application(path)
        if monitor_system:
            application.start_monitoring()
    return application

def get_system_item(path):
    """Returns the system item associated with the given path."""
    result = find_application(path)
    if not result:
        links = [link for link in get_links() if link.url == path]
        if len(links) > 0:
            result = links[0]
    if path and not result:
        result = get_application(path)
    return result

def get_application_from_xml(node):
    """Return an application from the given xml."""
    app_type = None
    for child_node in node.childNodes:
        if child_node.tagName == 'type':
            app_type = child_node.firstChild.nodeValue
        if child_node.tagName == 'name':
            name = child_node.firstChild.nodeValue

    if app_type in app_types:
        application = app_types[app_type].from_xml(node)
    else:
        application = NT_Application(name)
    return applications

def process_active(process):
    """Return a flag to indicate whether the given process is running."""
    return win32process.GetExitCodeProcess(process) == STILL_ACTIVE

version_info_strings = ('Comments',
                        'InternalName',
                        'ProductName',
                        'CompanyName',
                        'LegalCopyright',
                        'ProductVersion',
                        'FileDescription',
                        'LegalTrademarks',
                        'PrivateBuild',
                        'FileVersion',
                        'OriginalFilename',
                        'SpecialBuild')

def update_processes():
    """Return a list of path to executables for running processes."""
    pids = [process_id for process_id in win32process.EnumProcesses()]
    for pid in processes.keys():
        if pid not in pids:
            del processes[pid]
    for pid in pids:
        if pid not in processes or not process_active(processes[pid][0]):
            try:
                process = win32api.OpenProcess(win32security.POLICY_ALL_ACCESS, False, pid)
                modules = win32process.EnumProcessModules(process)
                path = win32process.GetModuleFileNameEx(process, modules[0])
                path = os.path.normcase(path)
                if path not in paths_to_ignore:
                    translations = win32api.GetFileVersionInfo(path, u'\\VarFileInfo\\Translation')
                    version_info = {}
                    lang, codepage = translations[0]
                    for string in version_info_strings:
                        version_info[string] = win32api.GetFileVersionInfo(path, u'\\StringFileInfo\\%04X%04X\\%s' % (lang, codepage, string))
                    processes[pid] = (process, path, version_info)
            except pywintypes.error:
               # Ignore access denied errors while attempting to connect to processes we may not own
               process = None
               path = None
    return processes

def get_applications():
    """Return a list of running applications."""
    update_processes()

    current_apps = set()
    for process, path, version_info in processes.values():
        application = get_application(path)
        if application.process != process:
            application._has_changed = True
            application.process = process
        application.name = version_info.get('FileDescription', application.name)
        application.version_info = version_info
        current_apps.add(application)

    return current_apps

def get_files():
    """Return a list of the visited files."""
    return set()

def filetime_to_datetime(ft):
    _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0)
    timestamp = ft.dwHighDateTime
    timestamp <<= 32
    timestamp |= ft.dwLowDateTime
    return _FILETIME_null_date + datetime.timedelta(seconds=-time.timezone, microseconds=timestamp / 10)

def get_links():
    """Return the list of visited URLs."""
    result = set()
    history = CreateObject(urlhistLib.UrlHistory)
    for item in history.EnumUrls():
        timestamp = (item.ftLastVisited.dwHighDateTime << 32) + item.ftLastVisited.dwLowDateTime
        pattern = str(item.pwcsUrl) + str(timestamp)
        if pattern not in known_urls:
            known_urls.add(pattern)            

            add_url = True
            for url in ignored_urls:
                if item.pwcsUrl.startswith(url):
                    add_url = False
                    break
            if add_url:
                time = filetime_to_datetime(item.ftLastVisited)
                result.add(Visited_Url(item.pwcsTitle, item.pwcsUrl, time))
    return result

def process_window(window, data):
    """Process windows returned by EnumWindows."""
    if win32gui.IsWindowVisible(window):
        print "%s : %s" % (win32gui.GetClassName(window), win32gui.GetWindowText(window))

    return True

def find_application_item(path):
    """Return an application item for the provided path."""
    application = None
    if os.path.exists(path):
        exe = os.path.basename(path).lower()
        if exe in custom_apps:
            detail = custom_apps[exe]
            application = app_types[detail[1]](path, *detail[2:])
        else:
            application = NT_Application(path)
    return application

def find_application(path):
    """Return an application item for the provided path."""
    app_paths = dict([(app.path, app) for app in known_applications])
    application = app_paths.get(path, None)
    return application

def show_reference(path):
    """Opens the path using the OS default application."""
    clsid = '{9BA05972-F6A8-11CF-A442-00A0C90A8F39}'
    shell_windows = win32com.client.Dispatch(clsid)
    for browser in shell_windows:
        if browser.LocationURL == path:
            win32gui.ShowWindow(browser.Application.HWND, win32con.SW_SHOW)
            win32gui.BringWindowToTop(browser.Application.HWND)
            break
    else:
        win32api.ShellExecute(0, None, path, None, os.getcwd(), 0)

def get_system_state():
    """Return a list of items representing the current system state."""
    state = set()
    state |= get_applications()
    state |= get_links()
    
    return state

monitor_system = False

def start_system_monitor():
    """Return a list of items representing the current system state."""
    global monitor_system
    monitor_system = True
    known_urls = set()
    get_links()
    for app in get_applications():
        app.start_monitoring()

def stop_system_monitor():
    """Return a list of items representing the current system state."""
    global monitor_system
    for app in get_applications():
        app.stop_monitoring()
    monitor_system = False

# Module test code
if __name__ == "__main__":
    import operator
    history = get_links(datetime.datetime.now() - datetime.timedelta(1))
    history.sort(key=operator.attrgetter('time'))
    print '\n'.join([str(item) for item in history])
